package com.gitee.qdbp.general.common.biz.files.service;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.gitee.qdbp.able.beans.KeyString;
import com.gitee.qdbp.able.exception.ServiceException;
import com.gitee.qdbp.general.common.api.files.model.FileAuthorization;
import com.gitee.qdbp.general.common.api.files.model.FolderName;
import com.gitee.qdbp.general.common.api.files.model.IPathParams;
import com.gitee.qdbp.general.common.api.files.model.PathParams;
import com.gitee.qdbp.general.common.api.files.service.IWebFileService;
import com.gitee.qdbp.general.common.error.FileErrorCode;
import com.gitee.qdbp.tools.files.FileTools;
import com.gitee.qdbp.tools.files.PathTools;
import com.gitee.qdbp.tools.instance.ExceptionLogger;
import com.gitee.qdbp.tools.instance.ExceptionLogger.CountLogWatcher;
import com.gitee.qdbp.tools.utils.ConvertTools;
import com.gitee.qdbp.tools.utils.RandomTools;
import com.gitee.qdbp.tools.utils.StringTools;
import com.gitee.qdbp.tools.utils.VerifyTools;

/**
 * 本地文件服务实现类<br>
 * 保存路径 = savePath + params.name + folder + fileName<br>
 * 返回 URL = baseUrl + params.name + folder + fileName<br>
 * 默认配置 savePath = .., baseUrl = /, 就是把文件上传到webapps目录下, 只适用于单tomcat的简单项目<br>
 * 也可以指定 savePath = /home/filecenter/, 再启一个nginx作图片服务器, baseUrl配置成 http://img.xx.com/<br>
 * 
 * <pre>
    # local.upload.base.url = http://img.xx.com/
    local.upload.base.url = /
    # local.upload.save.path = /home/filecenter/
    local.upload.save.path = ..
    # upload.dir: {year} {month} {day} {hour} {minute} {second} {millisecond} {random-1}~{random-10} {ext} {uuid}
    local.upload.dir = /{year}/{month}/{day}/
    # upload.name: {year} {month} {day} {hour} {minute} {second} {millisecond} {random-1}~{random-10} {ext} {uuid}
    local.upload.name = {hour}{minute}{second}{millisecond}{random-6}.{ext}
    # upload.size.limit: 0 = not limit, 1MB = 1 * 1024 * 1024
    local.upload.size.limit = 10 * 1024 * 1024

    &lt;bean class="com.gitee.qdbp.general.common.biz.files.service.LocalFileService"&gt;
        &lt;property name="baseUrl" value="${local.upload.base.url}" /&gt;
        &lt;property name="savePath" value="${local.upload.save.path}" /&gt;
        &lt;property name="folder" value="${local.upload.dir}" /&gt;
        &lt;property name="fileName" value="${local.upload.name}" /&gt;
        &lt;property name="sizeLimit" value="${local.upload.size.limit}" /&gt;
    &lt;/bean&gt;
 * </pre>
 * 
 * @author zhaohuihua
 * @version 160828
 */
//@Service
public class LocalFileService implements IWebFileService {

    /** 日志对象 **/
    private static final Logger log = LoggerFactory.getLogger(LocalFileService.class);

    /** 文件大小限制, 0表示无限制 **/
    private Long sizeLimit;
    /** 图片服务器基准路径 **/
    private String baseUrl;
    /** 文件保存路径 **/
    private String savePath;
    /** 上传文件的存储路径 **/
    private String folder;
    /** 上传文件的存储文件名 **/
    private String fileName;
    /** Web应用初始化时由InitServlet设置Web根路径 **/
    private String webRootPath;

    private static final Pattern PLACEHOLDER_CLEAR = Pattern.compile("\\{[\\w\\-\\.]*\\}");
    private static final Pattern BSLASH_CLEAR = Pattern.compile("\\\\");
    private static final Pattern REPETITION_SLASH_CLEAR = Pattern.compile("//+");

    public LocalFileService() {
        sizeLimit = 0L;
        baseUrl = "/";
        savePath = "..";
        folder = "/{year}/{month}/{day}/";
        fileName = "{hour}{minute}{second}{millisecond}{random-6}.{ext}";
    }

    /** {@inheritDoc} **/
    @Override
    public String upload(File file) throws ServiceException {
        return upload((IPathParams) null, file);
    }

    /** {@inheritDoc} **/
    @Override
    public String upload(String extension, byte[] bytes) throws ServiceException {
        return doUpload((IPathParams) null, extension, new ByteArrayInputStream(bytes), bytes.length);
    }

    /** {@inheritDoc} **/
    @Override
    public String upload(String extension, InputStream input, long size) throws ServiceException {
        return doUpload((IPathParams) null, extension, input, size);
    }

    /** {@inheritDoc} **/
    @Override
    public String upload(IPathParams params, File file) throws ServiceException {
        try (FileInputStream fileInputStream = new FileInputStream(file);
                BufferedInputStream bufferedStream = new BufferedInputStream(fileInputStream)) {
            return doUpload(params, file.getName(), bufferedStream, -1);
        } catch (IOException e) {
            throw new ServiceException(FileErrorCode.FILE_UPLOAD_ERROR, e);
        }
    }

    /** {@inheritDoc} **/
    @Override
    public String upload(IPathParams params, String extension, byte[] bytes) throws ServiceException {
        return doUpload(params, extension, new ByteArrayInputStream(bytes), bytes.length);
    }

    /** {@inheritDoc} **/
    @Override
    public String upload(IPathParams params, String extension, InputStream input, long size) throws ServiceException {
        return doUpload(params, extension, input, size);
    }

    /** {@inheritDoc} **/
    @Override
    public String upload(IPathParams params, String url) throws ServiceException {
        if (isInsideFile(url)) {
            if (isTempsFile(url)) {
                return moveTo(url, params);
            } else {
                return url;
            }
        } else {
            try (InputStream input = new URL(url).openStream()) {
                return doUpload(params, url, input, -1);
            } catch (IOException e) {
                throw new ServiceException(FileErrorCode.FILE_UPLOAD_ERROR, e);
            }
        }
    }

    private String doUpload(IPathParams params, String extension, InputStream input, long size)
            throws ServiceException {

        if (sizeLimit != null && sizeLimit > 0 && sizeLimit < size) {
            throw new ServiceException(FileErrorCode.FILE_SIZE_EXCEEDED);
        }

        // 根据配置规则生成文件保存路径
        String uri = generateUri(params, extension);
        String savePath = uriToSaveAbsolutePath(uri);

        if (log.isTraceEnabled()) {
            log.trace("local upload save path [{}].", new File(savePath).getAbsolutePath());
        }
        try { // 保存文件
            FileTools.saveFile(input, savePath);
        } catch (IOException e) {
            log.error("local upload save fail.", e);
            throw new ServiceException(FileErrorCode.FILE_UPLOAD_ERROR, e);
        }

        return PathTools.concat(this.baseUrl, uri);
    }

    /** 生成文件保存路径 **/
    public String generateSavePath(IPathParams params, String extension) {
        String uri = generateUri(params, extension);
        return uriToSaveAbsolutePath(uri);
    }

    /** URI转换为保存绝对路径 **/
    private String uriToSaveAbsolutePath(String uri) {
        if (PathTools.isAbsolutePath(this.savePath)) {
            // linux的绝对路径或D:/xxx/这样的路径
            return PathTools.concat(this.savePath, uri);
        } else {
            if (VerifyTools.isBlank(webRootPath)) {
                return PathTools.concat(this.savePath, uri);
            } else {
                return PathTools.concat(webRootPath, this.savePath, uri);
            }
        }
    }

    /** 根据配置规则生成URI路径 **/
    private String generateUri(IPathParams params, String extension) {
        String uri;
        String topFolder = params == null ? null : params.name();
        if (VerifyTools.isNotBlank(topFolder)) {
            uri = PathTools.concat(true, topFolder, folder, fileName);
        } else {
            uri = PathTools.concat(true, FolderName.files.name(), folder, fileName);
        }

        // 生成占位符的替换参数
        Map<String, Object> vars = new HashMap<>();
        // 日期时间
        Date date = new Date();
        for (KeyString i : TIME_PARTS) {
            vars.put(i.getKey(), new SimpleDateFormat(i.getValue()).format(date));
        }
        // 随机数1~10位
        String number = RandomTools.generateNumber(RANDOM_SIZE);
        for (int i = 1; i < RANDOM_SIZE; i++) {
            vars.put("random-" + i, number.substring(0, i));
        }
        vars.put("random-" + RANDOM_SIZE, number);
        vars.put("ext", PathTools.getExtension(extension, false));
        vars.put("uuid", RandomTools.generateUuid());
        if (params instanceof PathParams) {
            PathParams options = (PathParams) params;
            Map<String, Object> extra = options.getExtra();
            if (extra != null && !extra.isEmpty()) {
                vars.putAll(extra);
            }
        }

        String path = StringTools.format(uri, vars);
        path = PLACEHOLDER_CLEAR.matcher(path).replaceAll("");
        path = BSLASH_CLEAR.matcher(path).replaceAll("/");
        path = REPETITION_SLASH_CLEAR.matcher(path).replaceAll("/");
        return path;
    }

    /** {@inheritDoc} **/
    @Override
    public boolean isInsideFile(String url) throws ServiceException {
        VerifyTools.requireNotBlank(url, "url");
        return url.startsWith(baseUrl);
    }

    /** {@inheritDoc} **/
    @Override
    public boolean isTempsFile(String url) throws ServiceException {
        VerifyTools.requireNotBlank(url, "url");
        if (!isInsideFile(url)) {
            return false;
        }
        // 相对路径
        String relativePath = url.substring(baseUrl.length());
        if (relativePath.startsWith("/")) {
            relativePath = relativePath.substring(1);
        }
        return relativePath.startsWith(FolderName.temps.name() + "/");
    }

    /** 将path转换为本地绝对路径 **/
    @Override
    public String toLocalAbsolutePath(String path) {
        VerifyTools.requireNotBlank(path, "path");

        if (!isInsideFile(path)) return null;
        // baseUrl = http://img.xx.com/ 或 /filecenter/
        String baseUrl = getBaseUrl();
        String relativePath = path.substring(baseUrl.length());
        String savePath = getSavePath();
        if (PathTools.isAbsolutePath(savePath)) { // 绝对路径
            // savePath = /home/filecenter/
            return PathTools.concat(savePath, relativePath);
        } else { // 相对路径
            // savePath = ../filecenter/
            String serverPath = getWebRootPath();
            return PathTools.concat(serverPath, savePath, relativePath);
        }
    }

    /** {@inheritDoc} **/
    @Override
    public String moveTo(String url, IPathParams params) throws ServiceException {
        VerifyTools.requireNotBlank(url, "url");
        VerifyTools.requireNotBlank(params, "params");

        if (!isInsideFile(url)) {
            return url;
        }
        // 原路径相对路径
        String srcRelativePath = PathTools.formatPath(url.substring(baseUrl.length()));
        if (srcRelativePath.startsWith("/")) {
            srcRelativePath = srcRelativePath.substring(1);
        }
        // 判断文件是否存在
        File oraginal = new File(uriToSaveAbsolutePath(srcRelativePath));
        if (!oraginal.exists()) {
            log.trace("File move failed, {}, source not found!", oraginal.getAbsolutePath());
            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND);
        }

        // 根据源路径和带占位符的路径配置生成移动后的目标路径
        String placeholderPath = PathTools.concat(this.folder, this.fileName);
        String destRelativePath = FileHandleTools.generateMoveTargetPath(srcRelativePath, placeholderPath, params);

        if (srcRelativePath.equals(destRelativePath)) {
            return url; // 源文件与目标文件相同, 无需移动
        }
        File destination = new File(uriToSaveAbsolutePath(destRelativePath));
        if (destination.exists()) {
            log.trace("File move failed, {} --> {}, destination file exist!", srcRelativePath, destRelativePath);
            throw new ServiceException(FileErrorCode.FILE_ALREADY_EXISTS);
        }
        // 生成一个记录警告日志并统计失败次数的实例
        CountLogWatcher watcher = ExceptionLogger.newCountWatcher(false, this.getClass());
        // 执行文件移动操作
        FileTools.move(oraginal, destination, watcher);
        // 判断操作结果
        if (watcher.getFailedTimes() == 0) {
            log.trace("File move success, {} --> {}", srcRelativePath, destRelativePath);
        } else {
            throw new ServiceException(FileErrorCode.FILE_PROCESS_ERROR);
        }
        return PathTools.concat(baseUrl, destRelativePath);
    }

    /** {@inheritDoc} **/
    @Override
    public void deleteFile(String url) throws ServiceException {
        VerifyTools.requireNotBlank(url, "url");

        if (!isInsideFile(url)) {
            return;
        }
        // 相对路径
        String relativePath = PathTools.formatPath(url.substring(baseUrl.length()));
        // 判断文件是否存在
        File oraginal = new File(uriToSaveAbsolutePath(relativePath));
        if (!oraginal.exists()) {
            log.trace("File delete failed, {}, file not found!", relativePath);
            throw new ServiceException(FileErrorCode.FILE_NOT_FOUND);
        }

        // 生成一个记录警告日志并统计失败次数的实例
        CountLogWatcher watcher = ExceptionLogger.newCountWatcher(false, this.getClass());
        // 执行文件删除操作
        FileTools.delete(oraginal, watcher);
        // 判断操作结果
        if (watcher.getFailedTimes() == 0) {
            log.trace("File delete success, {}", relativePath);
        } else {
            throw new ServiceException(FileErrorCode.FILE_DELETE_ERROR);
        }
    }

    /** {@inheritDoc} **/
    @Override
    public FileAuthorization authorize() throws ServiceException {
        return authorize(null);
    }

    /** {@inheritDoc} **/
    @Override
    public FileAuthorization authorize(IPathParams params) throws ServiceException {
        throw new ServiceException(FileErrorCode.FILE_UPLOAD_NONSUPPORT);
    }

    /** {@inheritDoc} **/
    public String getWebRootPath() {
        return webRootPath;
    }

    /** 设置Web工程路径 **/
    public void setWebRootPath(String webRootPath) {
        this.webRootPath = webRootPath;
    }

    /** 设置文件大小限制, 0表示无限制 **/
    public void setSizeLimit(String sizeLimit) {
        if (VerifyTools.isNotBlank(sizeLimit)) {
            Long number = ConvertTools.toLong(sizeLimit);
            if (number != null) {
                this.sizeLimit = number;
            }
        }
    }

    /** 获取文件大小限制, 0表示无限制 **/
    public Long getSizeLimit() {
        return sizeLimit;
    }

    /** 设置图片服务器基准路径 **/
    public void setBaseUrl(String baseUrl) {
        if (VerifyTools.isNotBlank(baseUrl)) {
            this.baseUrl = baseUrl;
        }
    }

    /** 获取图片服务器基准路径 **/
    public String getBaseUrl() {
        return baseUrl;
    }

    /** 设置文件保存路径 **/
    public void setSavePath(String savePath) {
        if (VerifyTools.isNotBlank(savePath)) {
            this.savePath = savePath;
        }
    }

    /** 获取图片服务器基准路径 **/
    public String getSavePath() {
        return savePath;
    }

    /** 设置上传文件的存储路径 **/
    public void setFolder(String folder) {
        if (VerifyTools.isNotBlank(folder)) {
            this.folder = folder;
        }
    }

    /** 获取上传文件的存储路径 **/
    public String getFolder() {
        return folder;
    }

    /** 设置上传文件的存储文件名 **/
    public void setFileName(String fileName) {
        if (VerifyTools.isNotBlank(fileName)) {
            this.fileName = fileName;
        }
    }

    /** 获取上传文件的存储文件名 **/
    public String getFileName() {
        return fileName;
    }

    private static int RANDOM_SIZE = 10;
    private static List<KeyString> TIME_PARTS = new ArrayList<>();

    static { // 初始化时间占位符的格式化参数
        TIME_PARTS.add(new KeyString("year", "yyyy"));
        TIME_PARTS.add(new KeyString("month", "MM"));
        TIME_PARTS.add(new KeyString("day", "dd"));
        TIME_PARTS.add(new KeyString("hour", "HH"));
        TIME_PARTS.add(new KeyString("minute", "mm"));
        TIME_PARTS.add(new KeyString("second", "ss"));
        TIME_PARTS.add(new KeyString("millisecond", "SSS"));
    }
}
